package httpclient

import (
	"bytes"
	"gitee.com/tietang/terrace-go/v2/base/lb"
	"github.com/pkg/errors"
	"io"
	"io/ioutil"
	"net/http"
	"net/url"
	"strings"
	"time"
)

const (
	KeyAppName = "AppName"
)

var parseURL = url.Parse

// HttpClient is the http HttpClient implementation
type HttpClient struct {
	HttpClient Doer
	//httpClients map[string]Doer
	apps    *lb.Apps
	Options Options
}

const (
	defaultRetryCount  = 0
	defaultHTTPTimeout = 30 * time.Second
)

var _ Client = (*HttpClient)(nil)

// NewHttpClient returns a new instance of http HttpClient
func NewHttpClient(apps *lb.Apps, opts ...Options) *HttpClient {
	httpClient := HttpClient{
		apps: apps,
	}
	if len(opts) > 0 {
		httpClient.Options = opts[0]
	} else {
		httpClient.Options = Options{
			Timeout:    defaultHTTPTimeout,
			RetryCount: defaultRetryCount,
		}
	}

	if httpClient.HttpClient == nil {
		httpClient.HttpClient = &http.Client{
			Timeout: httpClient.Options.Timeout,
		}
	}

	return &httpClient
}

func (c *HttpClient) NewRequest(method, url string, body io.Reader) (*http.Request, error) {
	if method == "" {
		method = "GET"
	}
	//
	u, err := parseURL(url) // Just url.Parse (url is shadowed for godoc).
	if err != nil {
		return nil, err
	}
	//重置请求host
	name := u.Host
	//name := u.Hostname()
	app := c.apps.Get(name)
	if app == nil {
		return nil, errors.New("无可用应用，应用名称: " + name + "，请求：" + url)
	}
	ins := app.Get(url)
	if ins == nil {
		return nil, errors.New("无可用实例，应用名称: " + name + "，请求：" + url)
	}
	u.Host = ins.Address

	rc, ok := body.(io.ReadCloser)
	if !ok && body != nil {
		rc = ioutil.NopCloser(body)
	}
	// The host's colon:port should be normalized. See Issue 14836.
	//u.Host = removeEmptyPort(u.Host)
	req := &http.Request{
		Method:     method,
		URL:        u,
		Proto:      "HTTP/1.1",
		ProtoMajor: 1,
		ProtoMinor: 1,
		Header:     make(http.Header),
		Body:       rc,
		Host:       u.Host,
	}
	if body != nil {
		switch v := body.(type) {
		case *bytes.Buffer:
			req.ContentLength = int64(v.Len())
			buf := v.Bytes()
			req.GetBody = func() (io.ReadCloser, error) {
				r := bytes.NewReader(buf)
				return ioutil.NopCloser(r), nil
			}
		case *bytes.Reader:
			req.ContentLength = int64(v.Len())
			snapshot := *v
			req.GetBody = func() (io.ReadCloser, error) {
				r := snapshot
				return ioutil.NopCloser(&r), nil
			}
		case *strings.Reader:
			req.ContentLength = int64(v.Len())
			snapshot := *v
			req.GetBody = func() (io.ReadCloser, error) {
				r := snapshot
				return ioutil.NopCloser(&r), nil
			}
		default:
		}
		if req.GetBody != nil && req.ContentLength == 0 {
			req.Body = http.NoBody
			req.GetBody = func() (io.ReadCloser, error) { return http.NoBody, nil }
		}
	}
	req.Header.Add(KeyAppName, name)

	return req, nil
}

// Get makes a HTTP GET request to provided URL
func (c *HttpClient) Get(url string, headers http.Header) (*http.Response, error) {
	var response *http.Response
	request, err := c.NewRequest(http.MethodGet, url, nil)
	if err != nil {
		return response, errors.Wrap(err, "GET - request creation failed")
	}

	request.Header = headers

	return c.TryDo(request)
}

// Post makes a HTTP POST request to provided URL and requestBody
func (c *HttpClient) Post(url string, body io.Reader, headers http.Header) (*http.Response, error) {
	var response *http.Response
	request, err := c.NewRequest(http.MethodPost, url, body)
	if err != nil {
		return response, errors.Wrap(err, "POST - request creation failed")
	}

	request.Header = headers

	return c.TryDo(request)
}

// Put makes a HTTP PUT request to provided URL and requestBody
func (c *HttpClient) Put(url string, body io.Reader, headers http.Header) (*http.Response, error) {
	var response *http.Response
	request, err := c.NewRequest(http.MethodPut, url, body)
	if err != nil {
		return response, errors.Wrap(err, "PUT - request creation failed")
	}

	request.Header = headers

	return c.TryDo(request)
}

// Patch makes a HTTP PATCH request to provided URL and requestBody
func (c *HttpClient) Patch(url string, body io.Reader, headers http.Header) (*http.Response, error) {
	var response *http.Response
	request, err := c.NewRequest(http.MethodPatch, url, body)
	if err != nil {
		return response, errors.Wrap(err, "PATCH - request creation failed")
	}

	request.Header = headers

	return c.TryDo(request)
}

// Delete makes a HTTP DELETE request with provided URL
func (c *HttpClient) Delete(url string, headers http.Header) (*http.Response, error) {
	var response *http.Response
	request, err := c.NewRequest(http.MethodDelete, url, nil)
	if err != nil {
		return response, errors.Wrap(err, "DELETE - request creation failed")
	}

	request.Header = headers

	return c.TryDo(request)
}

func (c *HttpClient) TryDo(request *http.Request) (res *http.Response, err error) {
	appName := request.Header.Get(KeyAppName)
	var opt Option
	if o, ok := c.Options.Opts[appName]; ok {
		opt = o
	} else {
		opt.RetryCount = c.Options.RetryCount
		opt.Timeout = c.Options.Timeout
	}
	for i := 0; i <= opt.RetryCount; i++ {
		res, err = c.Do(request)

		if err != nil {
			continue
		}

		if res.StatusCode >= http.StatusInternalServerError {
			continue
		}

		return res, err
	}

	return res, err
}

// Do makes an HTTP request with the native `http.Do` interface
func (c *HttpClient) Do(request *http.Request) (*http.Response, error) {
	request.Close = true

	var bodyReader *bytes.Reader

	if request.Body != nil {
		reqData, err := ioutil.ReadAll(request.Body)
		if err != nil {
			return nil, err
		}
		bodyReader = bytes.NewReader(reqData)
		request.Body = ioutil.NopCloser(bodyReader) // prevents closing the body between retries
	}

	multiErr := &MultiError{}
	var response *http.Response

	if response != nil {
		response.Body.Close()
	}

	var err error
	response, err = c.HttpClient.Do(request)
	if bodyReader != nil {
		// Reset the body reader after the request since at this point it's already read
		// Note that it's safe to ignore the error here since the 0,0 position is always valid
		_, _ = bodyReader.Seek(0, 0)
	}

	if err != nil {
		return response, err
	}

	if response.StatusCode >= http.StatusInternalServerError {
		return response, err
	}

	multiErr = &MultiError{} // Clear errors if any iteration succeeds

	return response, multiErr.HasError()
}
